﻿using System.Net ;
using System.ComponentModel.Composition ;
using Microsoft.Practices.Prism.Modularity ;
using Microsoft.Practices.Prism.MefExtensions.Modularity ;
using System.Collections.Generic ;
using System.ComponentModel ;
using System.Windows ;
using System ;
using Microsoft.Practices.Prism.Logging;

namespace OpenLightGroup.Prism
{ 
    
    /// <summary>
    /// Provides a XAP type loader using the MEF DeploymentCatalog.
    /// </summary>
    /// <remarks>
    /// Used code from Prism source due to the fact that the MefXapModuleTypeLodader 
    /// does not provide the overrides necessary to implement this type. It is inherited from 
    /// MefXapModuleLoader so that the MEF export can be used. To update this class 
    /// with the newest version from the Prism source, simply copy and paste the new source 
    /// and replace all references to DeploymentCatalog with NetworkAwareDeploytmentCatalog
    /// </remarks>
    [Export(typeof(MefXapModuleTypeLoader))]
    public class CachingMefXapModuleTypeLoader : MefXapModuleTypeLoader, IModuleTypeLoader
    {
        
        /// <summary>
        /// Raised repeatedly to provide progress as modules are loaded in the background.
        /// </summary>
        public virtual event EventHandler<ModuleDownloadProgressChangedEventArgs> ModuleDownloadProgressChanged ;
        /// <summary>
        /// Raised when a module is loaded or fails to load.
        /// </summary>
        public virtual event EventHandler<LoadModuleCompletedEventArgs> LoadModuleCompleted ;
        private Dictionary<Uri, List<ModuleInfo>> downloadingModules = new Dictionary<Uri, List<ModuleInfo>>( ) ;
        private HashSet<Uri> downloadedUris = new HashSet<Uri>( ) ;
        private DownloadedPartCatalogCollection downloadedPartCatalogs ;
        private ILoggerFacade _logger;

        /// <summary>
        /// Initializes a new instance of the <see cref="MefXapModuleTypeLoader"/> class.
        /// </summary>
        /// <param name="downloadedPartCatalogs">The downloaded part catalog collection.</param>
        [ImportingConstructor]
        public CachingMefXapModuleTypeLoader ( DownloadedPartCatalogCollection downloadedPartCatalogs, ILoggerFacade logger )
            : base (downloadedPartCatalogs)
        {
            if ( downloadedPartCatalogs == null )
            {
                throw new ArgumentNullException( "downloadedPartCatalogs" ) ;
            }
            
            this._logger = logger ;

            this.downloadedPartCatalogs = downloadedPartCatalogs ;
        }

        /// <summary>
        /// Evaluates the <see cref="ModuleInfo.Ref"/> property to see if the current typeloader will be able to retrieve the <paramref name="moduleInfo"/>.
        /// Returns true if the <see cref="ModuleInfo.Ref"/> property is a URI, because this indicates that the file is a downloadable file. 
        /// </summary>
        /// <param name="moduleInfo">Module that should have it's type loaded.</param>
        /// <returns>
        /// 	<see langword="true"/> if the current typeloader is able to retrieve the module, otherwise <see langword="false"/>.
        /// </returns>
        public virtual bool CanLoadModuleType ( ModuleInfo moduleInfo )
        {
            if ( moduleInfo == null )
            {
                throw new ArgumentNullException( "moduleInfo" ) ;
            }

            if ( !string.IsNullOrEmpty( moduleInfo.Ref ) )
            {
                Uri uriRef ;
                return Uri.TryCreate( moduleInfo.Ref, UriKind.RelativeOrAbsolute, out uriRef ) ;
            }

            return false ;
        }

        /// <summary>
        /// Retrieves the <paramref name="moduleInfo"/>.
        /// </summary>
        /// <param name="moduleInfo">Module that should have it's type loaded.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are included in the resulting message and are handled by subscribers of the LoadModuleCompleted event." )]
        public virtual void LoadModuleType ( ModuleInfo moduleInfo )
        {            
            if ( moduleInfo == null )
            {
                throw new System.ArgumentNullException( "moduleInfo" ) ;
            }

            try
            {
                Uri uri = new Uri( moduleInfo.Ref, UriKind.RelativeOrAbsolute ) ;

                this.DownloadModuleFromUri( moduleInfo, uri ) ;
            }
            catch (Exception ex)
            {
                this._logger.Log( ex.Message, Category.Exception, Priority.High ) ;
                this.RaiseLoadModuleCompleted( moduleInfo, ex ) ;
            }
        }

        private void RaiseLoadModuleCompleted ( ModuleInfo moduleInfo, Exception error )
        {
            this.RaiseLoadModuleCompleted( new LoadModuleCompletedEventArgs( moduleInfo, error ) ) ;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The deplyment catalog is added to the container and disposed when the container is disposed." )]
        private void DownloadModuleFromUri ( ModuleInfo moduleInfo, Uri uri )
        {
            this._logger.Log( "Preparing to download " + moduleInfo.ModuleName + " from " + uri.ToString( ), Category.Debug, Priority.Low ) ;

            CachingDeploymentCatalog deploymentCatalog = new CachingDeploymentCatalog( uri ) ;       
            try
            {
                // If this module has already been downloaded, fire the completed event.
                if ( this.IsSuccessfullyDownloaded( deploymentCatalog.Uri ) )
                {
                    this._logger.Log( moduleInfo.ModuleName + " from " + uri.ToString() + " has already been downloaded.", Category.Debug, Priority.Low);

                    this.RaiseLoadModuleCompleted( moduleInfo, null ) ;
                }
                else
                {
                    bool needToStartDownload = !this.IsDownloading( uri ) ;

                    // I record downloading for the moduleInfo even if I don't need to start a new download
                    this.RecordDownloading( uri, moduleInfo ) ;

                    if ( needToStartDownload )
                    {
                        deploymentCatalog.DownloadProgressChanged += this.DeploymentCatalog_DownloadProgressChanged ;
                        deploymentCatalog.DownloadCompleted += this.DeploymentCatalog_DownloadCompleted ;
                        deploymentCatalog.DownloadAsync( ) ;
                    }
                }
            }
            catch (Exception ex)
            {
                // if there is an exception between creating the deployment catalog and calling DownloadAsync,
                // the deployment catalog needs to be disposed.
                // otherwise, it is added to the compositioncontainer which should handle this.
                deploymentCatalog.Dispose( ) ;
                
                this._logger.Log("Failed to download " + moduleInfo.ModuleName + " from " + uri.ToString(), Category.Exception, Priority.High);
                this._logger.Log( ex.Message, Category.Exception, Priority.High ) ;

                throw ;
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions are included in the resulting message and are handled by subscribers of the LoadModuleCompleted event." )]
        private void HandleDownloadCompleted ( CachingDeploymentCatalog deploymentCatalog, AsyncCompletedEventArgs e )
        {
            List<ModuleInfo> moduleInfos = this.GetDownloadingModules( deploymentCatalog.Uri ) ;
            
            Exception error = e.Error ;
            if ( error == null )
            {
                try
                {
                    this.RecordDownloadComplete( deploymentCatalog.Uri ) ;

                    foreach ( ModuleInfo moduleInfo in moduleInfos )
                    {
                        this.downloadedPartCatalogs.Add( moduleInfo, deploymentCatalog ) ;
                    }

                    this.RecordDownloadSuccess( deploymentCatalog.Uri ) ;

                }
                catch (Exception ex)
                {
                    error = ex ;
                }
            }

            foreach ( ModuleInfo moduleInfo in moduleInfos )
            {
                this.RaiseLoadModuleCompleted( moduleInfo, error ) ;
            }
        }

        private bool IsDownloading ( Uri uri )
        {
            lock ( this.downloadingModules )
            {
                return this.downloadingModules.ContainsKey( uri ) ;
            }
        }

        private List<ModuleInfo> GetDownloadingModules ( Uri uri )
        {
            lock ( this.downloadingModules )
            {
                return new List<ModuleInfo>( this.downloadingModules[uri] ) ;
            }
        }

        private bool IsSuccessfullyDownloaded ( Uri uri )
        {
            lock ( this.downloadedUris )
            {
                return this.downloadedUris.Contains( uri ) ;
            }
        }

        private void DeploymentCatalog_DownloadProgressChanged ( object sender, DownloadProgressChangedEventArgs e )
        {
            CachingDeploymentCatalog deploymentCatalog = (CachingDeploymentCatalog)sender ;

            // I ensure the download progress changed is raised is on the UI thread.
            if ( !Deployment.Current.Dispatcher.CheckAccess( ) )
            {
                Deployment.Current.Dispatcher.BeginInvoke( new Action<CachingDeploymentCatalog, DownloadProgressChangedEventArgs>( this.HandleDownloadProgressChanged ), deploymentCatalog, e ) ;
            }
            else
            {
                this.HandleDownloadProgressChanged( deploymentCatalog, e ) ;
            }
        }

        private void RaiseModuleDownloadProgressChanged ( ModuleDownloadProgressChangedEventArgs e )
        {
            if ( this.ModuleDownloadProgressChanged != null )
            {
                this.ModuleDownloadProgressChanged( this, e ) ;
            }
        }

        private void RaiseLoadModuleCompleted ( LoadModuleCompletedEventArgs e )
        {
            if ( this.LoadModuleCompleted != null )
            {
                this.LoadModuleCompleted( this, e ) ;
            }
        }

        private void RecordDownloading ( Uri uri, ModuleInfo moduleInfo )
        {
            this._logger.Log("Downloading " + moduleInfo.ModuleName + " from " + uri.ToString(), Category.Debug, Priority.Low);

            lock ( this.downloadingModules )
            {
                List<ModuleInfo> moduleInfos ;
                if ( !this.downloadingModules.TryGetValue( uri, out moduleInfos ) )
                {
                    moduleInfos = new List<ModuleInfo>( ) ;
                    this.downloadingModules.Add( uri, moduleInfos ) ;
                }

                if ( !moduleInfos.Contains( moduleInfo ) )
                {
                    moduleInfos.Add( moduleInfo ) ;
                }
            }
        }

        private void RecordDownloadSuccess ( Uri uri )
        {
            this._logger.Log("Downloaded " +  uri.ToString(), Category.Debug, Priority.Low);

            lock ( this.downloadedUris )
            {
                this.downloadedUris.Add( uri ) ;
            }
        }

        private void DeploymentCatalog_DownloadCompleted ( object sender, AsyncCompletedEventArgs e )
        {
            CachingDeploymentCatalog deploymentCatalog = (CachingDeploymentCatalog)sender ;
            if (e.Error != null)
            {
                this._logger.Log( e.Error.Message, Category.Exception, Priority.High ) ;
            }
            deploymentCatalog.DownloadProgressChanged -= DeploymentCatalog_DownloadProgressChanged ;
            deploymentCatalog.DownloadCompleted -= DeploymentCatalog_DownloadCompleted ;

            // I ensure the download completed is on the UI thread so that types can be loaded into the application domain.
            if ( !Deployment.Current.Dispatcher.CheckAccess( ) )
            {
                Deployment.Current.Dispatcher.BeginInvoke( new Action<CachingDeploymentCatalog, AsyncCompletedEventArgs>( this.HandleDownloadCompleted ), deploymentCatalog, e ) ;
            }
            else
            {
                this.HandleDownloadCompleted( deploymentCatalog, e ) ;
            }
        }

        private void RaiseModuleDownloadProgressChanged ( ModuleInfo moduleInfo, long bytesReceived, long totalBytesToReceive )
        {
            this.RaiseModuleDownloadProgressChanged( new ModuleDownloadProgressChangedEventArgs( moduleInfo, bytesReceived, totalBytesToReceive ) ) ;
        }

        private void RecordDownloadComplete ( Uri uri )
        {
            lock ( this.downloadingModules )
            {
                if ( !this.downloadingModules.ContainsKey( uri ) )
                {
                    this.downloadingModules.Remove( uri ) ;
                }
            }
        }

        private void HandleDownloadProgressChanged ( CachingDeploymentCatalog deploymentCatalog, DownloadProgressChangedEventArgs e )
        {
            List<ModuleInfo> moduleInfos = this.GetDownloadingModules( deploymentCatalog.Uri ) ;

            foreach ( ModuleInfo moduleInfo in moduleInfos )
            {
                this.RaiseModuleDownloadProgressChanged( moduleInfo, e.BytesReceived, e.TotalBytesToReceive ) ;
            }
        }
        
    }
}